package com.anji.hyperneat.modular;

import java.io.IOException;
import java.util.ArrayList;

import org.jgap.Chromosome;
import org.jgap.Configuration;

import com.anji.hyperneat.nd.ActivatorND;
import com.anji.hyperneat.nd.GridNetND;
import com.anji.hyperneat.nd.NDFloatArray;
import com.anji.hyperneat.nd.NDFloatArray.MatrixIterator;
import com.anji.hyperneat.nd.Util;
import com.anji.integration.Activator;
import com.anji.integration.TranscriberException;
import com.anji.persistence.Persistence;
import com.anji.util.DummyConfiguration;
import com.anji.util.Properties;

/**
 * Class for basic Multi-Net Hyperneat grid net transcription.
 * Includes support for:
 * 	Inputs:
 * 		Bias
 * 		Node Coordinates
 * 		Layer
 * 		Coordinate Deltas
 * 		Angles
 * 		Sub-net coordinates
 * Output
 * 		Weight
 * 		Bias
 * Reccurent networks currently not supported.
 * @author slusk
 *
 */
public class ModularHyperNeatMultiNetTranscriber extends AbstractModularHyperNeatMultiNetTranscriber {
	
	protected OutputMapper outputMapperWeight;
	protected OutputMapper outputMapperBias;
	
	protected BiasInputMapper biasInputMapper;
	protected SubNetCoordinateInputMapper subNetCoordsInputMapper;
	protected CoordinateInputMapper srcNodeCoordsInputMapper;
	protected CoordinateInputMapper tgtNodeCoordsInputMapper;
	protected FloatInputMapper layerInputMapper;
	protected CoordinateDeltaInputMapper coordDeltasInputMapper;
	protected FloatArrayInputMapper anglesInputMapper;	
	
	protected ArrayList<ICppnInputMapper> inputPlugins;
	
	protected int numCppnInputs;
	protected int numCppnOutputs;
	
	/**
	 * Constructor.
	 * @param props Config Properties from file.
	 */
	public ModularHyperNeatMultiNetTranscriber(Properties props) {
		super(props);
	}

	public void init(Properties props) {
		super.init(props);
		
		inputPlugins = new ArrayList<ICppnInputMapper>(7);
		
		// Setup output plugins
		outputMapperWeight = new OutputMapper();
		outputMapperBias= new OutputMapper();
		
		// Setup core input plugins: bia, subNetCoords, src/tgt node coords, layer
		
		biasInputMapper = new BiasInputMapper();
		inputPlugins.add(biasInputMapper);
		
		subNetCoordsInputMapper = new SubNetCoordinateInputMapper(multiNetDimensions);
		inputPlugins.add(subNetCoordsInputMapper);
		
		srcNodeCoordsInputMapper = new CoordinateInputMapper(layerDimensions, maxDimensions);
		inputPlugins.add(srcNodeCoordsInputMapper);
		
		tgtNodeCoordsInputMapper = new CoordinateInputMapper(layerDimensions, maxDimensions);
		inputPlugins.add(tgtNodeCoordsInputMapper);
		
		layerInputMapper = new FloatInputMapper(0, depth - 1);	// depth - 1 : highest possible value, for normalization
		inputPlugins.add(layerInputMapper);
		
		// setup optional input plugins, delta and angle
		
		if (includeDelta) {
			coordDeltasInputMapper = new CoordinateDeltaInputMapper(maxDimensions, -2, 2); // The absolute difference between two normalized inputs will be at most 2; so use -2..2 as the min/max vals
			inputPlugins.add(coordDeltasInputMapper);
		}
		
		if (includeAngle) {
			anglesInputMapper = new FloatArrayInputMapper(maxDimensions-1,(float)(0 - (Math.PI)), (float)Math.PI);	// atan2 produces values in the range of -PI to PI
			inputPlugins.add(anglesInputMapper);
		}
		
		numCppnInputs = 0;
		for (ICppnInputMapper inputPlugin : inputPlugins) {
			numCppnInputs += inputPlugin.getNumInputs();
		}
		
		numCppnOutputs = enableBias ? 2 : 1;
		
	}
	

	@Override
	protected ActivatorND createNet(Activator cppn, ActivatorND activatorND, int[] subNetCoords, Chromosome genotype) {
		
		if (null == activatorND) activatorND = setupActivator(genotype);		
		
		// Setup stuff
		CppnMapper cppnMapper = getCppnMapper(cppn);
		
		// Grab the weight array for coordinate iteration
		NDFloatArray[] weights = activatorND.getWeights();
		
		// Register Input Plugins
		registerInputPlugins(cppnMapper);
		
		// Register Output Plugins
		registerOutputPlugins(cppnMapper);
		
		for (int layer = 1; layer < depth; layer++) {
			for (MatrixIterator w = weights[layer-1].iterator(); w.hasNext(); w.next()) {
				// Pass the entire current state of the transcriber - mostly in case extending classes need easy access to anything
				populateCppnMapperInputs(cppn, activatorND, subNetCoords, cppnMapper, layer, w);
								
				// Query CPPN
				queryCppn(cppnMapper);
				
				// Pass the entire current state of the transcriber - mostly in case extending classes need easy access to anything
				processOutputsFromCppn(cppn, activatorND, subNetCoords, cppnMapper, layer, w);				
			}
		}

		return activatorND;
	}

	/**
	 * Create a new GridNetND activator for the HN algorithm to operate on; 
	 * weights, bias will be not be initialized.
	 * @param genotype The chromosome for this set of GridNets
	 * @return
	 */
	protected ActivatorND setupActivator(Chromosome genotype) {
		NDFloatArray[] weights = createWeightArray();
		NDFloatArray[] bias = createBiasArray();
		
		GridNetND net = new GridNetND(allLayersList, weights, bias, activationFunction, maxDimensions, 0, enableBias, "network " + genotype.getId() );
		
		return net;
	}

	/**
	 * Applies inputs from plugins to the cppn, activates it, then applies the 
	 * outputs from the cppn to their respective plugins.
	 * @param cppnMapper
	 */
	protected void queryCppn(CppnMapper cppnMapper) {
		cppnMapper.prepareInputs();
		cppnMapper.activate();
		cppnMapper.applyOutputs();
	}

	/**
	 * Creates a cppn mapper from a cppn.
	 * @param cppn
	 * @return
	 */
	protected CppnMapper getCppnMapper(Activator cppn) {
		return new CppnMapper(cppn);
	}

	/**
	 * Takes the Cppn output for the weight, and checks that has greater magnitude than
	 * the connection expression threshold; if so, normalizes it to be between 0 and maxconnection weight 
	 * (if positive; min conn weight if negative); otherwise, sets it to 0.
	 * @param value The cppn output value for the weight.
	 * @return
	 */
	protected float translateOutputToWeightValue(float value) {
		float weightVal;

		weightVal = Math.min(connectionWeightMax, Math.max(connectionWeightMin, value));

		// check if wgt val is > connection expression threshold
		if (Math.abs(weightVal) > connectionExprThresh) {
			// if so, normalize it in the appropriate direction +/-
			if (weightVal > 0)
				weightVal = (weightVal - connectionExprThresh) * (connectionWeightMax / (connectionWeightMax - connectionExprThresh));
			else
				weightVal = (weightVal + connectionExprThresh) * (connectionWeightMin / (connectionWeightMin + connectionExprThresh));
		} 
		// Otherwise, connection not strong enough, set to 0
		else {
			weightVal = 0;
		}
		
		return weightVal;
	}

	/**
	 * Takes outputs provided to output plugins and does the appropriate things with them,
	 * e.g., setting weight values; uses the current transcriber "state" to make sure all
	 * information and objects needed are available.
	 * @param cppn The cppn activator; provided just in case, for class extension.
	 * @param activatorND The up-and-coming Net; provided for getting/setting things on it.
	 * @param subNetCoords The coordinates of the current subnet; just in case.
	 * @param cppnMapper The cppn mapper; just in case.
	 * @param layer The current net layer under going processing.
	 * @param w The weight iterator, for applying things to the right coordinate.  Probably not a good idea to actually iterate with it.
	 */
	protected void processOutputsFromCppn(Activator cppn, ActivatorND activatorND, int[] subNetCoords, CppnMapper cppnMapper, int layer, MatrixIterator w) {
		// Handle weight
		float weight = translateOutputToWeightValue( outputMapperWeight.getValue() );
		w.set(weight);
		
		int[] coords = w.getCurrentCoordinates();
		if (isCoordsForBias(coords) && enableBias) {
			float biasVal = getBiasValue();
			setBiasAtCoords(activatorND.getBias()[layer-1], coords, biasVal);
		}
	}

	/**
	 * Takes the Cppn output for the bias, and checks that has greater magnitude than
	 * the connection expression threshold; if so, normalizes it to be between 0 and maxconnection weight 
	 * (if positive; min conn weight if negative); otherwise, sets it to 0.
	 * @return
	 */
	protected float getBiasValue() {
		float biasVal;

		biasVal = Math.min(connectionWeightMax, Math.max(connectionWeightMin, outputMapperBias.getValue()));

		if (Math.abs(biasVal) > connectionExprThresh) {
			if (biasVal > 0)
				biasVal = (biasVal - connectionExprThresh) * (connectionWeightMax / (connectionWeightMax - connectionExprThresh));
			else
				biasVal = (biasVal + connectionExprThresh) * (connectionWeightMin / (connectionWeightMin + connectionExprThresh));
		} else {
			biasVal = 0;
		}
		return biasVal;
	}

	/**
	 * Populates the input plugins with their appropriate values prior to feeding to the cppn; 
	 * provides state and objects so everything is accessible.
	 * @param cppn Cppn activator; just in case.
	 * @param activatorND The up-and-coming net; just in case.
	 * @param subNetCoords The current sub-net coordinates.
	 * @param cppnMapper The cppn mapper; just in case.
	 * @param layer The current layer.
	 * @param w The current weight iterator; mostly for retrieving coordinates; probably not a good idea to iterate off it.
	 */
	protected void populateCppnMapperInputs(Activator cppn, ActivatorND activatorND, int[] subNetCoords, CppnMapper cppnMapper, int layer, MatrixIterator w) {
		// nothing extra needed for biasInputMapper
		
		subNetCoordsInputMapper.populateValues(subNetCoords);
		
		int[] wCoords = w.getCurrentCoordinates();
		srcNodeCoordsInputMapper.populateValues(layer-1, Util.getSrcCoordsFromCoordsSet(wCoords));
		tgtNodeCoordsInputMapper.populateValues(layer, Util.getTgtCoordsFromCoordsSet(wCoords));
		layerInputMapper.populateValue(layer);
		
		if (includeDelta) {
			populateCoordinateDeltas(tgtNodeCoordsInputMapper.getNormalizedInputs(), srcNodeCoordsInputMapper.getNormalizedInputs());
		}
		
		if (includeAngle) {
			populateAngles(tgtNodeCoordsInputMapper.getNormalizedInputs(), srcNodeCoordsInputMapper.getNormalizedInputs());
		}	
	}

	/**
	 * Populate angle plugin with angle values based on the current layer and set of src-tgt coords.
	 * @param layer The layer 
	 * @param w the current weight iterator, to retrieve source/target coordinates from.
	 */
	protected void populateAngles(float[] normTgtCoords, float[] normSrcCoords) {
		float[] array = new float[maxDimensions-1];
		for (int d = 1; d < maxDimensions; d++) {
			float angle = (float) Math.atan2(normTgtCoords[d-1] - normSrcCoords[d-1], normTgtCoords[d] - normSrcCoords[d]);
			
			angle /= 2 * (float) Math.PI;
			if (angle < 0)
				angle += 1;
			
			array[d-1] = angle; 
		}
		anglesInputMapper.populateValues(array);
	}

	/**
	 * Populate the Coordinate deltas plugin, using the differences between the current sets of coordinates.
	 * @param normTgtCoords Target node coordinates
	 * @param normSrcCoords Source Node coordiantes.
	 */
	protected void populateCoordinateDeltas(float[] normTgtCoords, float[] normSrcCoords) {
		coordDeltasInputMapper.populateValues(normTgtCoords, normSrcCoords);
	}

	/**
	 * Register input plugins for the CPPNMapper;
	 * pulls from inputPlugins list.
	 * @param cppnMapper
	 */
	protected void registerInputPlugins(CppnMapper cppnMapper) {
		for (ICppnInputMapper plugin : inputPlugins) {
			cppnMapper.registerInputPlugin(plugin);
		}		
	}

	/**
	 * Register output plugins for the CPPNMapper;
	 * In this case only registers weight, and if enabled, bias
	 * @param cppnMapper
	 */
	protected void registerOutputPlugins(CppnMapper cppnMapper) {
		cppnMapper.registerOutputPlugin(outputMapperWeight);
		if (enableBias) 
			cppnMapper.registerOutputPlugin(outputMapperBias);
	}

	/**
	 * For testing only.
	 * @param args
	 * @throws IOException
	 * @throws TranscriberException 
	 */
	public static void main(String[] args) throws IOException, TranscriberException {
		// Setup
		Properties props = new Properties("properties/modTest.properties");
		ModularHyperNeatMultiNetTranscriber xcriber = new ModularHyperNeatMultiNetTranscriber(props);
		
		Persistence db = (Persistence) props.newObjectProperty( Persistence.PERSISTENCE_CLASS_KEY );
		Configuration config = new DummyConfiguration();
		Chromosome chrom = db.loadChromosome("10160", config);
		
		// Test methods from base class
		/*
//		 * createBiasArray
		NDFloatArray[] biasArray = xcriber.createBiasArray();
		
//		 * createWeightArray
		NDFloatArray[] weightArray = xcriber.createWeightArray();
		
//		 * getCppnActivator
		Activator cppn = xcriber.getCppnActivator(chrom);
		
//		 * getDepth
		int depth = xcriber.getDepth();
		
//		 * getPhenotypeClass
		Class c = xcriber.getPhenotypeClass();
		
//		 * isCoordsForBias
		int[] testCoords = new int[] {0,1,2,0,0,1,2,0};
		int[] testCoords2 = new int[] {0,1,2,0,0,1,2,1};
		boolean isBias = xcriber.isCoordsForBias(testCoords);
		boolean isBias2 = xcriber.isCoordsForBias(testCoords2);
		
//		 * setBiasAtCoords
		xcriber.setBiasAtCoords(biasArray[0], testCoords, 0.5f);
		
//		 
		
		
//		 * transcribe
		
//		 * transcribe
		 
		// Test local methods
		
//		 * getBiasValue
		xcriber.outputMapperBias.accept(0.25f);
		float biasVal = xcriber.getBiasValue();
		
//		 * getCppnMapper
		CppnMapper mapper = xcriber.getCppnMapper(cppn);
		
//		 * populateAngles
		float[] normTgtCoords = new float[] {0.0f,1.0f, 0.5f, 0.0f};
		float[] normSrcCoords = new float[] {-0.5f,1.0f, -1.0f, 0.0f};
		xcriber.populateAngles(normTgtCoords, normSrcCoords);	// now check angle mapper
		
//		 * populateCoordinateDeltas
		xcriber.populateCoordinateDeltas(normTgtCoords, normSrcCoords);
						

		
//		 * registerInputPlugins
		xcriber.registerInputPlugins(mapper);
		
//		 * registerOutputPlugins
		xcriber.registerOutputPlugins(mapper);
		
//		 * setupActivator
		ActivatorND activatorND = xcriber.setupActivator(chrom);
		
//		 * translateOutputToWeightValue
		 float weight = xcriber.translateOutputToWeightValue(0.5f);
		 
//		 * populateCppnMapperInputs
//		xcriber.populateCppnMapperInputs(cppn, activatorND, subNetCoords, cppnMapper, layer, w);
		 
//		 * queryCppn
//			xcriber.queryCppn(mapper);
			
//		 * processOutputsFromCppn
//		xcriber.processOutputsFromCppn(cppn, activatorND, subNetCoords, cppnMapper, layer, w);
		*/
		//* newGridNetArray
		
		
		// xcriber.newGridNetArray(chrom, null);
		
		Activator cppn = xcriber.getCppnActivator(chrom);
		ActivatorND net = xcriber.createNet(cppn, null, new int[] {0,0}, chrom);
		
	}

	@Override
	public int getNumCppnInputs() {
		return numCppnInputs;
	}

	@Override
	public int getNumCppnOutputs() {
		return numCppnOutputs;
	}
}
